/*
 * Run CGI scripts with the permissions of their owner
 *
 * Copyright 2022, 2023, and 2024 Odin Kroeger.
 *
 * This file is part of suCGI.
 *
 * suCGI is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * suCGI is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with suCGI. If not, see <https://www.gnu.org/licenses/>.
 */

#define _BSD_SOURCE
#define _DARWIN_C_SOURCE
#define _DEFAULT_SOURCE
#define _GNU_SOURCE

#if defined(__OPTIMIZE__) && !defined(_FORTIFY_SOURCE)
#define _FORTIFY_SOURCE 3
#endif

#include <sys/stat.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <limits.h>
#include <inttypes.h>
#include <grp.h>
#include <pwd.h>
#include <regex.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

#include "attr.h"
#include "env.h"
#include "error.h"
#include "handler.h"
#include "macros.h"
#include "params.h"
#include "path.h"
#include "priv.h"
#include "str.h"
#include "types.h"
#include "user.h"


/*
 * Constants
 */

/* suCGI version */
#define VERSION "0"


/*
 * Module variables
 */

/* Regular expressions that define which environment variables are kept */
static const char *const safe_var_patterns[] = SAFE_ENV_VARS;

/* Filename suffix-script interpreter pairs */
static const Pair script_handlers[] = SCRIPT_HANDLERS;


/*
 * Prototypes
 */

/* Print help and exit with EXIT_SUCCESS */
_noreturn
static void help(void);

/* Print build configuration and exit with EXIT_SUCCESS */
_noreturn
static void config(void);

/* Print version and exit with EXIT_SUCCESS */
_noreturn
static void version(void);

/* Print usage information to stderr and exit with EXIT_FAILURE */
_noreturn
static void usage(void);


/*
 * Main
 */

int
main(int argc, char **argv)
{
    Error retval;


    /*
     * Check build configuration.
     */

    /*
     * The maximum values of uid_t, gid_t, GRP_T, and NGRPS_T may have
     * been guessed at compile-time. However, these guesses do not account
     * for padding bits. So it must be tested whether each maximum value
     * can, in fact, be represented by the type it is supposed to be
     * the maximum value of.
     */

    /* cppcheck-suppress misra-c2012-10.4; checking for change is the point */
    ASSERT((uid_t) MAX_UID_VAL == MAX_UID_VAL);
    /* cppcheck-suppress misra-c2012-10.4; -- " -- */
    ASSERT((gid_t) MAX_GID_VAL == MAX_GID_VAL);
    /* cppcheck-suppress misra-c2012-10.4; -- " -- */
    ASSERT((GRP_T) MAX_GRP_VAL == MAX_GRP_VAL);
    /* cppcheck-suppress misra-c2012-10.4; -- " -- */
    ASSERT((NGRPS_T) MAX_NGRPS_VAL == MAX_NGRPS_VAL);

    ASSERT(sizeof(GRP_T) == sizeof(gid_t));
    ASSERT(MAX_NGRPS_VAL >= INT_MAX);

    /* NOLINTBEGIN(bugprone-sizeof-expression); nothing bug-prone about it */

    ASSERT(sizeof(USER_DIR) > 1U);
    ASSERT(sizeof(USER_DIR) <= (size_t) MAX_FNAME_LEN);
    /* cppcheck-suppress sizeofwithnumericparameter; NOLINTNEXTLINE */
    ASSERT(sizeof(PATH) > 0U);
    /* cppcheck-suppress sizeofwithnumericparameter */
    ASSERT(sizeof(PATH) <= (uintmax_t) MAX_FNAME_LEN);
#if defined(ARG_MAX) && ARG_MAX > -1
    /* cppcheck-suppress sizeofwithnumericparameter */
    ASSERT(sizeof(PATH) <= (uintmax_t) ARG_MAX);
#endif
    ASSERT(sizeof(BASE_URL) > 1U);
    ASSERT(sizeof(BASE_URL) <= (size_t) MAX_URL_LEN);

    /* NOLINTEND(bugprone-sizeof-expression) */

    /*
     * setreuid and setregid accept -1 as ID. However, POSIX.1-2008 allows
     * for uid_t, gid_t, and id_t to be unsigned integers, and this is how
     * they are defined on most systems. On these systems, -1 wraps around
     * to the maximum value that the type can represent. So that value is
     * not a valid ID.
     */

    ASSERT((uintmax_t) STOP_UID <= ((uintmax_t) MAX_UID_VAL - 1U));
    ASSERT((uintmax_t) STOP_GID <= ((uintmax_t) MAX_GID_VAL - 1U));
    ASSERT((uintmax_t) STOP_GID <= ((uintmax_t) MAX_GRP_VAL - 1U));

    /* cppcheck-suppress misra-c2012-10.4; checking for change is the point */
    ASSERT((uid_t) START_UID == START_UID);
    /* cppcheck-suppress misra-c2012-10.4; -- " -- */
    ASSERT((uid_t)  STOP_UID ==  STOP_UID);
    /* cppcheck-suppress misra-c2012-10.4; -- " -- */
    ASSERT((gid_t) START_GID == START_GID);
    /* cppcheck-suppress misra-c2012-10.4; -- " -- */
    ASSERT((gid_t)  STOP_GID ==  STOP_GID);
    /* cppcheck-suppress misra-c2012-10.4; -- " -- */
    ASSERT((GRP_T) START_GID == START_GID);
    /* cppcheck-suppress misra-c2012-10.4; -- " -- */
    ASSERT((GRP_T)  STOP_GID ==  STOP_GID);
    /* cppcheck-suppress misra-c2012-10.4; -- " -- */
    ASSERT((NGRPS_T) INT_MAX == INT_MAX);


    /*
     * Words of wisdom from the authors of suEXEC:
     *
     * > While cleaning the environment, the environment should be clean.
     * > (E.g. malloc() may get the name of a file for writing debugging
     * > info. Bad news if MALLOC_DEBUG_FILE is set to /etc/passwd.)
     */

    char *const *vars = environ;
    char *null = NULL;

    environ = &null;


    /*
     * Set up logging.
     */

    openlog("sucgi", SYSLOG_OPTS, SYSLOG_FACILITY);

    errno = 0;
    /* cppcheck-suppress misra-config; Cppcheck does not find closelog */
    if (atexit(closelog) != 0) {
        error("atexit: %m");
    }

    (void) setlogmask(SYSLOG_MASK);


    /*
     * Drop privileges temporarily.
     */

    retval = priv_suspend();
    if (retval != OK) {
        /* NOTREACHED */
        BUG("priv_suspend() -> %d [!]", retval);
    }


    /*
     * Options.
     */

    if (argc == 0 || argv == NULL || *argv == NULL || **argv == '\0') {
        error("Empty argument vector");
    }

    if (argc > 2) {
        usage();
    }

    /* Some versions of getopt are insecure */
    for (int i = 1; i < argc; ++i) {
        const char *const arg = argv[i];

        assert(arg != NULL);

        if (strncmp("-c", arg, sizeof("-c")) == 0) {
            config();
        } else if (strncmp(arg, "-h", sizeof("-h")) == 0 ||
                   strncmp(arg, "--help", sizeof("--help")) == 0) {
            help();
        } else if (strncmp(arg, "-v", sizeof("-v")) == 0 ||
                   strncmp(arg, "--version", sizeof("--version")) == 0) {
            version();
        } else {
            usage();
        }
    }


    /*
     * Restore environment.
     */

    /* cppcheck-suppress misra-config; the size of this array is fine */
    regex_t safe_var_pregs[NELEMS(safe_var_patterns)];
    for (size_t i = 0; i < NELEMS(safe_var_patterns); ++i) {
        int err = regcomp(&safe_var_pregs[i], safe_var_patterns[i],
                          REG_EXTENDED | REG_NOSUB);
        if (err != 0) {
            /* RATS: ignore; regerror respects the size of errmsg */
            char errmsg[MAX_ERRMSG_LEN] = {0};

            /* Error messages may be truncated */
            (void) regerror(err, &safe_var_pregs[i], errmsg,
                            sizeof(errmsg) - 1U);
            error("regcomp: %s", errmsg);
        }
    }

    retval = env_init();
    switch (retval) {
    case OK:
        break;
    case ERR_BAD:
        error("Malformed variable in minimal environment");
    case ERR_LEN:
        error("Variable or variable name in minimal environment is too long");
    case ERR_NELEMS:
        error("Too many variables in minimal environment");
    case ERR_SYS:
        error("setenv: %m");
    default:
        /* NOTREACHED */
        BUG("env_init() -> %d [!]", retval);
    }

    retval = env_restore((const char *const *) vars,
                         NELEMS(safe_var_pregs), safe_var_pregs);
    switch (retval) {
    case OK:
        break;
    case ERR_NELEMS:
        error("Too many environment variables");
    case ERR_SYS:
        error("setenv: %m");
    default:
        /* NOTREACHED */
        BUG("env_restore(%p, %zu, %p) -> %d [!]",
            vars, NELEMS(safe_var_pregs), safe_var_pregs, retval);
    }


    /*
     * Get the script's filename and filesystem metadata.
     */

    errno = 0;
    /* cppcheck-suppress misra-c2012-21.8; PATH_INFO is used for validation */
    const char *const pathinfo = getenv("PATH_INFO"); /* RATS: ignore */
    if (pathinfo == NULL) {
        /* cppcheck-suppress misra-c2012-22.10; getenv may set errno */
        if (errno == 0) {
            error("$PATH_INFO: Not set");
        } else {
            error("getenv: %m");
        }
    }

    if (*pathinfo == '\0') {
        error("$PATH_INFO: Empty");
    }

    /* FIXME: This condition is not tested for */
    if (strnlen(pathinfo, MAX_URL_LEN) >= MAX_URL_LEN) {
        error("$PATH_INFO: Too long");
    }

    errno = 0;
    /* cppcheck-suppress misra-c2012-21.8; CGI requires using getenv */
    const char *const script = getenv("PATH_TRANSLATED");  /* RATS: ignore */
    if (script == NULL) {
        /* cppcheck-suppress misra-c2012-22.10; getenv may set errno */
        if (errno == 0) {
            error("$PATH_TRANSLATED: Not set");
        } else {
            error("getenv: %m");
        }
    }

    const size_t scriptlen = strnlen(script, MAX_FNAME_LEN);
    if (scriptlen >= (size_t) MAX_FNAME_LEN) {
        error("$PATH_TRANSLATED: Too long");
    }

    if (scriptlen == 0U) {
        error("$PATH_TRANSLATED: Empty");
    }

    char *realscript = NULL;
    size_t realscriptlen = 0;
    retval = path_get_real(scriptlen, script,
                           &realscriptlen, &realscript);
    switch (retval) {
    case OK:
        break;
    case ERR_SYS:
        error("realpath %s: %m", script);
    default:
        /* NOTREACHED */
        BUG("path_get_real(%zu, \"%s\", -> %zu, -> \"%s\") -> %d [!]",
            scriptlen, script,
            realscriptlen, realscript, retval);
    }

    struct stat scriptstatus;
    if (stat(realscript, &scriptstatus) != 0) {
        error("stat %s: %m", script);
    }

    if ((scriptstatus.st_mode & S_IFREG) == 0) {
        error("script %s: Not a regular file", script);
    }


    /*
     * Check wether the script is owned by a privileged user.
     */

    errno = 0;
    /* cppcheck-suppress getpwuidCalled; used safely */
    const struct passwd *owner = getpwuid(scriptstatus.st_uid);
    if (owner == NULL) {
        /* cppcheck-suppress misra-c2012-22.10; getpwuid sets errno */
        if (errno == 0) {
            error("script %s: No owner", script);
        } else {
            error("getpwuid: %m");
        }
    }

    assert(owner->pw_uid == scriptstatus.st_uid);

    if (owner->pw_uid < START_UID || owner->pw_uid > STOP_UID) {
        error("script %s: Owned by privileged user", script);
    }

    const char *const logname = owner->pw_name;
    const uid_t uid = owner->pw_uid;
    const gid_t gid = owner->pw_gid;


    /*
     * Check the owner's group memberships.
     */

    gid_t groups[MAX_NGROUPS];
    for (size_t i = 0; i < NELEMS(groups); ++i) {
        groups[i] = (gid_t) NOGROUP;
    }

    int ngroups = NELEMS(groups);

    /*
     * GRP_T names the data type that getgrouplist takes and returns GIDs as.
     * On older systems and macOS this is int, on modern systems gid_t.
     *
     * Casting gid_t to GRP_T is guaranteed to be safe because:
     * (1) gid_t and GRP_T use the same representation for any value
     *     in [START_GID, STOP_GID] (so type-casting cannot change values).
     * (2) a run-time error is raised if a GID falls outside that range.
     * (3) a compile-time error is raised if sizeof(GRP_T) != sizeof(gid_t)
     *     (so GRP_T[i] and gid_t[i] cannot refer to different addresses).
     *
     * gid_t and GRP_T are guaranteed to use the same representation for any
     * value in the above range because a compile-time error is raised if:
     * (1) START_GID < 1 (so type-casting cannot change the sign);
     * (2) STOP_GID > the highest value that gid_t or GRP_T can represent
     *     (so values cannot overflow).
     */
    (void) getgrouplist(logname, (GRP_T) gid, (GRP_T *) groups, &ngroups);

    if (ngroups < 0) {
        /* NOTREACHED */

        /* cppcheck-suppress cert-INT31-c; cast is safe */
        if (ISSIGNED(gid_t)) {
            BUG("getgrouplist(\"%s\", %lld, %p, -> %d < 0 [!])",
                logname, (long long) gid, groups, ngroups);
        } else {
            BUG("getgrouplist(\"%s\", %llu, %p, -> %d < 0 [!])",
                logname, (unsigned long long) gid, groups, ngroups);
        }
    }

    long maxngroups = sysconf(_SC_NGROUPS_MAX);
    if (maxngroups < 0L || (uintmax_t) maxngroups > (uintmax_t) MAX_NGROUPS) {
        maxngroups = (long) MAX_NGROUPS;
    }

    if (maxngroups < ngroups) {
        /* maxngroups must be <= INT_MAX if this point is reached */
        assert((uintmax_t) maxngroups <= (uintmax_t) INT_MAX);

        /* RATS: ignore; message is short and a literal */
        syslog(LOG_WARNING, "user %s: can only set %ld out of %d groups",
               logname, maxngroups, ngroups);

        ngroups = (int) maxngroups;
    }

    for (int i = 0; i < ngroups; ++i) {
        if ((uintmax_t) groups[i] < (uintmax_t) START_GID || 
	        (uintmax_t) groups[i] > (uintmax_t) STOP_GID)
	    {
            /* cppcheck-suppress getgrgidCalled; used safely */
            const struct group *const grp = getgrgid(groups[i]);
            if (grp != NULL) {
                error("user %s: Member of system group %s",
                      logname, grp->gr_name);
            /* cppcheck-suppress cert-INT31-c; cast is safe */
            } else if (ISSIGNED(gid_t)) {
                error("user %s: Member of system group %lld",
                      logname, (long long) groups[i]);
            } else {
                error("user %s: Member of system group %llu",
                      logname, (unsigned long long) groups[i]);
            }
        }
    }


    /*
     * Drop privileges for good.
     */

    errno = 0;
    if (seteuid(0) != 0) {
        error("seteuid: %m");
    }

    /*
     * NGRPS_T names the data type of setgroups third argument, the number of
     * groups given. On GNU-like systems this is size_t, on others int.
     *
     * Casting ngroups to NGRPS_T is guaranteed to be safe because:
     * (1) ngroups cannot be negative (so values cannot change sign);
     * (2) ngroups is capped at MAX_NGROUPS and a compile-time error
     *     is raised if MAX_NGROUPS > INT_MAX or if NGRPS_T cannot
     *     represent INT_MAX (so values cannot overflow).
     */
    retval = priv_drop(uid, gid, (NGRPS_T) ngroups, groups);
    switch (retval) {
    case OK:
        break;
    case ERR_SYS:
        error("Could not drop privileges: %m");
    default:
        /* NOTREACHED */
        /* cppcheck-suppress cert-INT31-c; not an integer conversion */
        if (ISSIGNED(id_t)) {
            BUG("priv_drop(%lld, %lld, %d, %p) -> %d [!]",
                (long long) uid, (long long) gid,
                (int) ngroups, groups, retval);
        } else {
            BUG("priv_drop(%llu, %llu, %d, %p) -> %d [!]",
                (unsigned long long) uid, (unsigned long long) gid,
                (int) ngroups, groups, retval);
        }
    }


    /*
     * Check whether the script is within the user directory.
     */

    /* RATS: ignore; user_exp respects the size of userdir */
    char userdir[MAX_FNAME_LEN];
    size_t userdirlen = 0;
    retval = user_exp(USER_DIR, owner, sizeof(userdir), userdir, &userdirlen);
    switch (retval) {
    case OK:
        break;
    case ERR_BAD:
        error("user %s: Login name or home directory is too long", logname);
    case ERR_LEN:
        error("user %s: User directory is too long", logname);
    case ERR_SYS:
        error("snprintf: %m");
    default:
        /* NOTREACHED */
        BUG("user_exp(\"%s\", <user %s>, %zu, -> \"%s\", -> %zu) -> %d [!]",
            USER_DIR, owner->pw_name, sizeof(userdir),
            userdir, userdirlen, retval);
    }

    char *realuserdir = NULL;
    size_t realuserdirlen = 0;
    errno = 0;
    retval = path_get_real(userdirlen, userdir, &realuserdirlen, &realuserdir);
    switch (retval) {
    case OK:
        break;
    case ERR_LEN:
        error("user %s: User directory too long", logname);
    case ERR_SYS:
        error("realpath %s: %m", userdir);
    default:
        /* NOTREACHED */
        BUG("path_get_real(%zu, \"%s\", -> %zu, -> \"%s\") -> %d [!]",
            userdirlen, userdir, realuserdirlen, realuserdir, retval);
    }

    if (!path_is_sub(realscriptlen, realscript, realuserdirlen, realuserdir)) {
        error("script %s: Not in %s's user directory", script, logname);
    }


    /*
     * It would be odd for the set-user-ID or the set-group-ID on execute
     * bits to be set for a script that is owned by a regular user. So if
     * one of them is set, this probably indicates a configuration error.
     */

    if ((scriptstatus.st_mode & S_ISUID) != 0) {
        error("script %s: Set-user-ID on execute bit set", script);
    }

    if ((scriptstatus.st_mode & S_ISGID) != 0) {
        error("script %s: Set-group-ID on execute bit set", script);
    }


    /*
     * There should be no need to run hidden files or files that reside
     * in hidden directories. So if the path of the script does refer to
     * a hidden file, this probably indicates a configuration error.
     */

    if (strstr(realscript, "/.") != NULL) {
        error("script %s: Hidden", script);
    }


    /*
     * Script URL
     */

    /* RATS: ignore; user_exp respects the size of scripturl. */
    char scripturl[MAX_URL_LEN];
    size_t baseurllen = 0;
    retval = user_exp(BASE_URL, owner, sizeof(scripturl), scripturl, &baseurllen);
    switch (retval) {
    case OK:
        break;
    case ERR_BAD:
        /* TODO: Have more error types */
        error("user %s: Login name or home directory is too long", logname);
    case ERR_LEN:
        error("user %s: User directory is too long", logname);
    case ERR_SYS:
        error("snprintf: %m");
    default:
        /* NOTREACHED */
        BUG("user_exp(\"%s\", <user %s>, %zu, -> \"%s\", -> %zu) -> %d [!]",
            USER_DIR, owner->pw_name, sizeof(scripturl),
            scripturl, baseurllen, retval);
    }

    /* TODO: Factor this out into a function.  */
    const size_t scripturlsize = sizeof(scripturl) - baseurllen - 1U;
    const char *const relativescript = &realscript[realuserdirlen];
    /* cppcheck-suppress misra-c2012-11.5; correct pointer type */
    const char *const end = memccpy(&scripturl[baseurllen], relativescript,
                                    '\0', scripturlsize);

    if (!end) {
        error("script %s: Filename is too long", script);
    }

    /* cppcheck-suppress misra-c2012-18.4; safe pointer subtraction */
    ptrdiff_t scripturllen = end - scripturl - 1;

    assert(scripturllen >= 0);
    assert((uintmax_t) scripturllen <= (uintmax_t) SIZE_MAX);
    assert(strnlen(scripturl, MAX_STR_LEN) == (size_t) scripturllen);

    if (strncmp(pathinfo, scripturl, (size_t) scripturllen) != 0) {
        error("$PATH_INFO: Does not start with %s", scripturl);
    }


    /*
     * Environment
     */

    errno = 0;
    if (setenv("DOCUMENT_ROOT", realuserdir, true) != 0) {
        error("setenv: %m");
    }

    errno = 0;
    if (setenv("HOME", owner->pw_dir, true) != 0) {
        error("setenv: %m");
    }

    errno = 0;
    if (setenv("PATH", PATH, true) != 0) {
        error("setenv: %m");
    }

    errno = 0;
    if (setenv("PATH_TRANSLATED", realscript, true) != 0) {
        error("setenv: %m");
    }

    errno = 0;
    if (setenv("SCRIPT_FILENAME", realscript, true) != 0) {
        error("setenv: %m");
    }

    errno = 0;
    if (setenv("SCRIPT_NAME", scripturl, true) != 0) {
        error("setenv: %m");
    }

    errno = 0;
    if (setenv("USER_NAME", logname, true) != 0) {
        error("setenv: %m");
    }

    errno = 0;
    if (chdir(realuserdir) != 0) {
        error("chdir %s: %m", realuserdir);
    }

    /* RATS: ignore; the permission mask is set by the administrator */
    umask(UMASK);


    /*
     * Run the script.
     */

    const char *scripthandler = NULL;
    retval = handler_find(NELEMS(script_handlers), script_handlers,
                          realscriptlen, realscript, &scripthandler);
    switch (retval) {
    case OK:
        errno = 0;
        /* RATS: ignore; suCGI's whole point is to do this safely */
        (void) execlp(scripthandler, scripthandler, realscript, NULL);
        /* cppcheck-suppress unreachableCode; is reached if execlp fails */
        error("execlp %s %s: %m", scripthandler, script);
    case ERR_BAD:
        error("script %s: Bad handler", script);
    case ERR_LEN:
        error("script %s: Filename suffix too long", script);
    case ERR_SEARCH:
        ; /* Falls through */
    case ERR_SUFFIX:
        break;
    default:
        /* NOTREACHED */
        BUG("handler_find(%zu, %p, %s, %p): %d",
            NELEMS(script_handlers), script_handlers,
            realscript, scripthandler, retval);
    }

    errno = 0;
    /* RATS: ignore; suCGI's whole point is to do this safely */
    (void) execl(realscript, realscript, NULL);
    /* cppcheck-suppress unreachableCode; is reached if execl fails */
    error("execl %s: %m", script);
}


/*
 * Functions
 */

static void
help(void)
{
    (void) printf(
"suCGI - run CGI scripts with the permissions of their owner\n\n"
"Usage:  sucgi [-c|-h|-v]\n\n"
"Options:\n"
"    -c  Print the build configuration.\n"
"    -h  Print this help screen.\n"
"    -v  Print version and license information.\n\n"
"Home page: <https://codeberg.org/odkr/sucgi>\n"
    );

    /* cppcheck-suppress misra-c2012-21.8; least bad option */
    exit(EXIT_SUCCESS);
}

static void
config(void)
{
    (void) printf("# Configuration\n");

    (void) printf("USER_DIR='%s'\n",   USER_DIR);
    (void) printf("START_UID=%llu\n",  (unsigned long long) START_UID);
    (void) printf("STOP_UID=%llu\n",   (unsigned long long) STOP_UID);
    (void) printf("START_GID=%llu\n",  (unsigned long long) START_GID);
    (void) printf("STOP_GID=%llu\n",   (unsigned long long) STOP_GID);

    (void) printf("SCRIPT_HANDLERS='\n");
    for (size_t i = 0; i < NELEMS(script_handlers); ++i) {
        (void) printf("\t%s=%s\n",
                      script_handlers[i].key,
                      script_handlers[i].value);
    }
    (void) printf("'\n");

    (void) printf("SAFE_ENV_VARS='\n");
    for (size_t i = 0; i < NELEMS(safe_var_patterns); ++i) {
        (void) printf("\t%s\n", safe_var_patterns[i]);
    }
    (void) printf("'\n");

    (void) printf("SYSLOG_FACILITY=%d\n", SYSLOG_FACILITY);
    (void) printf("SYSLOG_MASK=%d\n",     SYSLOG_MASK);
    (void) printf("SYSLOG_OPTS=%d\n",     SYSLOG_OPTS);

    /* cppcheck-suppress invalidPrintfArgType_s; false positive */
    (void) printf("PATH='%s'\n", PATH);
    (void) printf("UMASK=0%o\n", (unsigned) UMASK);

    (void) printf("\n# Limits\n");

    (void) printf("MAX_STR_LEN=%llu\n",
                  (unsigned long long) MAX_STR_LEN);
    (void) printf("MAX_ERRMSG_LEN=%llu\n",
                  (unsigned long long) MAX_ERRMSG_LEN);
    (void) printf("MAX_FNAME_LEN=%llu\n",
                  (unsigned long long) MAX_FNAME_LEN);
    (void) printf("MAX_LOGNAME_LEN=%llu\n",
                  (unsigned long long) MAX_LOGNAME_LEN);
    (void) printf("MAX_SUFFIX_LEN=%llu\n",
                  (unsigned long long) MAX_SUFFIX_LEN);
    (void) printf("MAX_VAR_LEN=%llu\n",
                  (unsigned long long) MAX_VAR_LEN);
    (void) printf("MAX_VARNAME_LEN=%llu\n",
                  (unsigned long long) MAX_VARNAME_LEN);
    (void) printf("MAX_NGROUPS=%llu\n",
                  (unsigned long long) MAX_NGROUPS);
    (void) printf("MAX_NVARS=%llu\n",
                  (unsigned long long) MAX_NVARS);
    (void) printf("MAX_UID_VAL=%llu\n",
                  (unsigned long long) MAX_UID_VAL);
    (void) printf("MAX_GID_VAL=%llu\n",
                  (unsigned long long) MAX_GID_VAL);
    (void) printf("MAX_GRP_VAL=%llu\n",
                  (unsigned long long) MAX_GRP_VAL);
    (void) printf("MAX_NGRPS_VAL=%llu\n",
                  (unsigned long long) MAX_NGRPS_VAL);

    (void) printf("\n# System\n");

#if defined(LIBC)
    /* cppcheck-suppress invalidPrintfArgType_s; false positive */
    (void) printf("LIBC=%s\n", LIBC);
#else
    (void) printf("#LIBC=???\n");
#endif

    (void) printf("\n# Debugging\n");

#if defined(TESTING)
    (void) printf("TESTING=%d\n", TESTING);
#else
    (void) printf("TESTING=0\n");
#endif

#if defined(NATTR)
    (void) printf("NATTR=%d\n", NATTR);
#else
    (void) printf("NATTR=0\n");
#endif

#if defined(NDEBUG)
    (void) printf("NDEBUG=%d\n", NDEBUG);
#else
    (void) printf("NDEBUG=0\n");
#endif

    /* cppcheck-suppress misra-c2012-21.8; least bad option */
    exit(EXIT_SUCCESS);
}

static void
version(void)
{
    (void) printf(
"suCGI %s\n"
"Copyright 2022, 2023, and 2024 Odin Kroeger.\n"
"Released under the GNU Affero General Public License.\n"
"This programme comes with ABSOLUTELY NO WARRANTY.\n",
    VERSION);

    /* cppcheck-suppress misra-c2012-21.8; least bad option */
    exit(EXIT_SUCCESS);
}

static void
usage(void)
{
    (void) fprintf(stderr, "usage: sucgi [-c|-h|-v]\n");

    /* cppcheck-suppress misra-c2012-21.8; least bad option */
    exit(2);
}


